# coding: utf-8
# 鲲鹏邮件群发器
# by redice

import sys
from PyQt4 import QtGui, QtCore
import ConfigParser
from collections import deque
import os
import re
import smtplib
from email.mime.text import MIMEText
import threading
import time


# 邮件群发线程类
class Worker(QtCore.QThread):
    emails = deque()
    
    def __init__(self, parent = None):
        QtCore.QThread.__init__(self, parent)
        self.exiting = False
        self.sendinterval = 2
        self.host = ''
        self.port = ''
        self.user = ''
        self.passwd = ''
        self.postfix = ''
        self.subject = ''
        self.sender = ''
        self.content = ''
        self.charset = ''
        self.log = None
    
    # 设置发送参数、启动线程
    def render(self, subject, sender, content, charset='gbk', host='localhost', port=25, user='', passwd='', postfix='', sendinterval=2, log=None):
        self.subject = subject
        self.sender = sender
        self.content = content
        self.charset = charset
        self.host = host
        self.port = port
        self.user = user
        self.passwd = passwd
        self.postfix = postfix
        self.sendinterval = 2
        self.log = log
        self.start()
    
    def stop(self):
        self.__del__()
        
    def __del__(self):
        self.exiting = True
        self.wait()
        
    def run(self):
        mail_list = list()
        num = 0
        while not self.exiting and Worker.emails:
            num = num +1
            mail_list.append(Worker.emails.popleft())
                
            if num>=10 or (not Worker.emails and num>0):
                time.sleep(self.sendinterval)
                ret = send_mail(to_list=mail_list, sub=self.subject, content=self.content, charset=self.charset, sender=self.sender, \
                                mail_host=self.host, mail_port=self.port, mail_user=self.user, mail_pass=self.passwd, mail_postfix=self.postfix)
                # 发送成功
                if ret==True:
                    self.emit(QtCore.SIGNAL("output(int, int)"), num, 0)
                else:
                    # 发送失败
                    self.emit(QtCore.SIGNAL("output(int, int)"), 0, num)
                    if self.log:
                        self.log.write('发送失败：%s，%s' % (str(mail_list), str(ret)))

                mail_list = list()
                num = 0

# 警告提示框
def alert(msg):
    QtGui.QMessageBox.critical(self, u'警告', msg)
    return

# 主应用程序窗口类
class MainWindow(QtGui.QMainWindow):
    email_re = re.compile('[\w\.\+-]{1,64}@\w[\w\.\+-]{1,255}\.\w+')
   
    def __init__(self):
       
        QtGui.QMainWindow.__init__(self)
        
        self.setWindowTitle(u'鲲鹏邮件群发器V1.0')
        self.statusBar().showMessage(u'鲲鹏Web数据采集 - 专业Web数据采集服务提供者  http://www.site-digger.com')
        self.setWindowIcon(QtGui.QIcon('icons/email.ico'))
        self.trayIcon = QtGui.QSystemTrayIcon(QtGui.QIcon('icons/email.ico'), self)
        self.trayIcon.setToolTip(u'鲲鹏邮件群发器V1.0\n鲲鹏Web数据采集 - 专业Web数据采集服务提供者\nhttp://www.site-digger.com')
        self.trayIcon.show()
        self.layout()
        
        QtCore.QObject.connect(self.trayIcon, QtCore.SIGNAL("activated(QSystemTrayIcon::ActivationReason)"), self.__icon_activated)
        
        self.emails_totalnum = 0
        self.emails_sendednum = 0
        self.emails_sendedoknum = 0
        self.emails_sendederrnum = 0
        self.emails_deque = deque()
        self.threads = list()
        self.send_log = open('send_log.log', 'w')
   
    # 设置布局
    def layout(self):
        gBox = QtGui.QGroupBox()
        self.setCentralWidget(gBox)
        
        subject = QtGui.QLabel(u'邮件主题：')
        sender = QtGui.QLabel(u'发送者：')
        receiver = QtGui.QLabel(u'接收者：')
        charset = QtGui.QLabel(u'邮件编码：')
        content = QtGui.QLabel(u'邮件正文：')
        
        self.subjectEdit = QtGui.QLineEdit()
        self.senderEdit = QtGui.QLineEdit()
        self.receiverEdit = QtGui.QLineEdit()
        self.charsetEdit = QtGui.QComboBox()
        self.contentEdit = QtGui.QTextEdit()
       
        self.startbtn = QtGui.QPushButton(u'开始发送')
        self.stopbtn = QtGui.QPushButton(u'停止发送')
        self.stopbtn.setEnabled(False)
        
        self.pbar = QtGui.QProgressBar(self)
        self.charsetEdit.addItems([u'gbk', u'big5', u'utf-8'])
        
        grid = QtGui.QGridLayout()
        grid.setSpacing(10)
        
        grid.addWidget(subject,1, 0)
        grid.addWidget(self.subjectEdit, 1 ,1 ,1, 10)
        
        grid.addWidget(sender, 2, 0)
        grid.addWidget(self.senderEdit, 2, 1, 1, 10)
        
        grid.addWidget(receiver, 3, 0)
        grid.addWidget(self.receiverEdit, 3, 1, 1, 10)
        
        grid.addWidget(charset, 4, 0)
        grid.addWidget(self.charsetEdit, 4, 1, 1, 10)
        
        grid.addWidget(content, 5, 0)
        grid.addWidget(self.contentEdit, 5, 1, 10, 10)
        
        grid.addWidget(self.startbtn, 16, 1, 1, 1)
        grid.addWidget(self.stopbtn, 16, 2, 1, 1)
        grid.addWidget(self.pbar, 16, 3, 1, 8)
        
        gBox.setLayout(grid)
        
        self.settingAction = QtGui.QAction(QtGui.QIcon('icons/setting.ico'), u'SMTP参数设置', self)
        self.settingAction.setShortcut('Alt+S')
        self.connect(self.settingAction, QtCore.SIGNAL('triggered()'), self.showSetting)
        
        self.optionsAction = QtGui.QAction(QtGui.QIcon('icons/options.ico'), u'其它参数设置', self)
        self.optionsAction.setShortcut('Alt+O')
        self.connect(self.optionsAction, QtCore.SIGNAL('triggered()'), self.showOptions)

        self.loadingAction = QtGui.QAction(QtGui.QIcon('icons/loading.ico'), u'加载邮件列表', self)
        self.loadingAction.setShortcut('Alt+L')
        self.connect(self.loadingAction, QtCore.SIGNAL('triggered()'), self.loadReceiver)
        
        self.helpAction = QtGui.QAction(QtGui.QIcon('icons/help.ico'), u'帮助', self)
        self.helpAction.setShortcut('Alt+H')
        self.connect(self.helpAction, QtCore.SIGNAL('triggered()'), self.about)
        
        self.exitAction = QtGui.QAction(QtGui.QIcon('icons/exit.ico'), u"退出", self)
        self.exitAction.setShortcut('Alt+Q')
        self.connect(self.exitAction, QtCore.SIGNAL('triggered()'), self.exitOkay)

        self.quitAction = QtGui.QAction(u"退出", self)
        self.connect(self.quitAction, QtCore.SIGNAL('triggered()'), self.exitOkay)
        
        self.aboutAction = QtGui.QAction(u"关于", self)
        self.connect(self.aboutAction, QtCore.SIGNAL('triggered()'), self.about)
        
        self.trayIconMenu = QtGui.QMenu(self)
        self.trayIconMenu.addAction(self.aboutAction)
        self.trayIconMenu.addAction(self.quitAction)
        self.trayIcon.setContextMenu(self.trayIconMenu)
        
        oprateMenu = self.menuBar().addMenu(u'操作')
        oprateMenu.addAction(self.loadingAction)
        oprateMenu.addAction(self.exitAction)

        settingMenu = self.menuBar().addMenu(u'设置')
        settingMenu.addAction(self.settingAction)
        settingMenu.addAction(self.optionsAction)
        
        self.menuBar().addMenu(u'帮助').addAction(self.helpAction)
        
        self.connect(self.startbtn,  QtCore.SIGNAL('clicked()'), self.sendmail)
        self.connect(self.stopbtn,  QtCore.SIGNAL('clicked()'), self.stopsend)
  
        self.setFixedSize(800, 600)
        self.center()
        
        if os.path.exists(u'about.txt'):
           self.contentEdit.setText(to_unicode(open(u'readme.txt', 'r').read(), 'gbk'))
    
    # 窗体居中
    def center(self):
        screen = QtGui.QDesktopWidget().screenGeometry()
        size = self.geometry()
        self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2)
 
    # 显示设置窗口、模态
    def showSetting(self):
        self.sw = SettingWindow()
        self.sw.exec_()

    # 显示选项窗口、模态
    def showOptions(self):
        self.ow = OptionsWindow()
        self.ow.exec_()
    
    # 加载收信人列表
    def loadReceiver(self):
        filename = to_unicode(QtGui.QFileDialog.getOpenFileName(self, u'选择邮件列表文件', filter="Txt Files (*.txt)").toUtf8().data(), 'utf-8')
        if not filename: return
        self.emails_deque.extend(open(filename, 'r').readlines())
      
        if self.emails_deque:
            self.receiverEdit.setText(u'共加载邮件数：%s，邮件列表文件：%s' % (len(self.emails_deque), filename))
    
    # 显示帮助页
    def about(self):
        import webbrowser
        webbrowser.open_new_tab('http://www.site-digger.com/?from=mailsender')

    # 强制结束发送线程
    def stopsend(self):
        for thread in self.threads:
            thread.terminate()
    
        self.threads = list()
        self.startbtn.setEnabled(True)
        self.stopbtn.setEnabled(False)
        self.statusBar().showMessage(u'邮件发送已被强制终止。')

    # 开始发送邮件
    def sendmail(self):
                
        if self.subjectEdit.text()=='':
            QtGui.QMessageBox.critical(self, u'警告', u'请先填写邮件主题！')
            self.subjectEdit.setFocus()
            return
        else:
            subject = to_unicode(self.subjectEdit.text().toUtf8().data(), 'utf-8')
        
        if self.senderEdit.text()=='':
            QtGui.QMessageBox.critical(self, u'警告', u'请先填写发送者信息！')
            self.senderEdit.setFocus()
            return
        else:
            sender = to_unicode(self.senderEdit.text().toUtf8().data(), 'utf-8')
        
        if not self.emails_deque:
            if  not self.email_re.search(self.receiverEdit.text()):
                QtGui.QMessageBox.critical(self, u'警告', u'请先加载邮件列表或填写接收者邮箱！')
                return
            self.emails_deque.append(str(self.receiverEdit.text()))
        
        if self.contentEdit.toPlainText()=='':
            QtGui.QMessageBox.critical(self, u'警告', u'请先填写邮件内容！')
            self.contentEdit.setFocus()
            return
        else:
            content = to_unicode(self.contentEdit.toPlainText().toUtf8().data(), 'utf-8')
            
        charset = str(self.charsetEdit.currentText())
        
        subject = subject.encode(charset)
        sender = sender.encode(charset)
        content = content.encode(charset)
    
        # disable startbtn, enable stopbtn
        self.startbtn.setEnabled(False)
        self.stopbtn.setEnabled(True)
        
        self.emails_sendednum = 0
        self.emails_sendedoknum = 0
        self.emails_sendederrnum = 0
        self.emails_totalnum = len(self.emails_deque)
        Worker.emails = self.emails_deque
        self.pbar.setMaximum(self.emails_totalnum)
        
        cfg = ConfigParser.ConfigParser()
        cfg.read('setting.ini')
        
        try:
            host = cfg.get('setting', 'host')
        except Exception, e:
            host = 'localhost'
        
        try:
            port = cfg.get('setting', 'port')
        except Exception, e:
            port = 25
        
        try:
            user = cfg.get('setting', 'user')
        except Exception, e:
            user = ''
        
        try:
            passwd = cfg.get('setting', 'pass')
        except Exception, e:
            passwd = ''
        
        try:
            postfix = cfg.get('setting', 'postfix')
        except Exception, e:
            postfix = ''
        
        try:
            threadnum = int(cfg.get('setting', 'threadnum'))
        except Exception, e:
            threadnum = 2
        
        try:
            sendinterval = int(cfg.get('setting', 'sendinterval'))
        except Exception, e:
            sendinterval = 2
        
        self.threads = [Worker() for i in range(threadnum)]
        for thread in self.threads:
            self.connect(thread, QtCore.SIGNAL("output(int, int)"), self.showProcess)
            thread.render(subject=subject, sender=sender, content=content, charset=charset, host=host, \
                          port=port, user=user, passwd=passwd, postfix=postfix, sendinterval=sendinterval, log=self.send_log)

    # 显示发送进程，响应线程通知消息
    def showProcess(self, oknum, errnum):
        # 已发送完毕总数
        self.emails_sendednum = self.emails_sendednum + oknum + errnum
        # 成功总数
        self.emails_sendedoknum = self.emails_sendedoknum + oknum
        # 失败总数
        self.emails_sendederrnum = self.emails_sendederrnum + errnum
        
        self.pbar.setValue(self.emails_sendednum)
        self.statusBar().showMessage(u'正在发送邮件... 总计：%d个，已发送：%d个(成功：%d个，失败：%d个)' % \
                                     (self.emails_totalnum, self.emails_sendednum, self.emails_sendedoknum, self.emails_sendederrnum))
        
        if self.emails_sendednum >= self.emails_totalnum:
            # enable startbtn, disable startbtn
            self.startbtn.setEnabled(True)
            self.stopbtn.setEnabled(False)
            self.statusBar().showMessage(u'邮件发送已完成。')
            self.trayIcon.showMessage(u'鲲鹏邮件群发器', u'发送完毕啦！', QtGui.QSystemTrayIcon.Information)
    
    # 托盘图标事件响应
    def __icon_activated(self, reason):
        if reason == QtGui.QSystemTrayIcon.Trigger or reason == QtGui.QSystemTrayIcon.DoubleClick:
            self.show()

    # 关闭窗口事件响应
    def closeEvent(self, event):
        self.hide()
        self.trayIcon.showMessage(u'鲲鹏邮件群发器', u'我并没有退出，我在这儿...', QtGui.QSystemTrayIcon.Information)
        event.ignore()

    # 退出应用程序确认
    def exitOkay(self):
        tipstr = u"确认退出程序?"
        
        if self.emails_totalnum>0 and self.emails_sendednum<self.emails_totalnum:
            tipstr = u"邮件尚未发送完毕，确认退出程序?"
        
        if QtGui.QMessageBox.question(self, u'确认退出？', tipstr, QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) == QtGui.QMessageBox.Yes:
            for thread in self.threads:
                thread.stop()
            QtGui.qApp.quit()


# 设置窗口类
class SettingWindow(QtGui.QDialog):
    def __init__(self, parent=None):
        QtGui.QDialog.__init__(self, parent)
        self.setWindowTitle(u'SMTP参数设置')
        self.setWindowIcon(QtGui.QIcon('icons/email.ico'))
        self.layout()
        self.loadSetting()
 
    # 设置窗口布局
    def layout(self):
        host = QtGui.QLabel(u'SMTP主机：')
        port = QtGui.QLabel(u'SMTP端口：')
        user = QtGui.QLabel(u'SMTP用户名：')
        pwd = QtGui.QLabel(u'用户密码：')
        postfix = QtGui.QLabel(u'邮箱后缀：')
        
        self.hostEdit = QtGui.QLineEdit()
        self.portEdit = QtGui.QLineEdit()
        self.userEdit = QtGui.QLineEdit()
        self.pwdEdit = QtGui.QLineEdit()
        self.pwdEdit.setEchoMode(QtGui.QLineEdit.Password)
        self.postfixEdit = QtGui.QLineEdit()
        
        save = QtGui.QPushButton(u'保存')
        cancel = QtGui.QPushButton(u'取消')
        
        grid = QtGui.QGridLayout()
        grid.setSpacing(10)
  
        grid.addWidget(host, 1, 0)
        grid.addWidget(self.hostEdit, 1, 1, 1, 3)
        
        grid.addWidget(port, 2, 0)
        grid.addWidget(self.portEdit, 2, 1, 1, 3)
        
        grid.addWidget(user, 3, 0)
        grid.addWidget(self.userEdit, 3, 1, 1, 3)
        
        grid.addWidget(pwd, 4, 0)
        grid.addWidget(self.pwdEdit, 4, 1, 1, 3)
        
        grid.addWidget(postfix, 5, 0)
        grid.addWidget(self.postfixEdit, 5, 1, 1, 3)
        
        grid.addWidget(save, 6, 1)
        grid.addWidget(cancel, 6, 2)
        
        self.connect(cancel, QtCore.SIGNAL('clicked()'), QtCore.SLOT('close()'))
        self.connect(save, QtCore.SIGNAL('clicked()'), self.saveSetting)
        
        self.setLayout(grid)
        self.setFixedSize(350, 250)
        self.center()
    
    # 窗口居中
    def center(self):
        screen = QtGui.QDesktopWidget().screenGeometry()
        size = self.geometry()
        self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2)
        
    # 加载设置
    def loadSetting(self):
        cfg = ConfigParser.ConfigParser()
        cfg.read('setting.ini')
        
        try:
            host = cfg.get('setting', 'host')
        except Exception, e:
            host = ''
            
        try:
            port = cfg.get('setting', 'port')
        except Exception, e:
            port = ''
        
        try:
            user = cfg.get('setting', 'user')
        except Exception, e:
            user = ''
        
        try:
            passwd = cfg.get('setting', 'pass')
        except Exception, e:
            passwd = ''
        
        try:
            postfix = cfg.get('setting', 'postfix')
        except Exception, e:
            postfix = ''
        
        self.hostEdit.setText(host)
        self.portEdit.setText(port)
        self.userEdit.setText(user)
        self.pwdEdit.setText(passwd)
        self.postfixEdit.setText(postfix)
       
    # 保存设置
    def saveSetting(self):
        if self.hostEdit.text()=='':
            QtGui.QMessageBox.critical(self, u'警告', u'SMTP主机不能为空！')
            return
        
        if self.portEdit.text()=='':
            QtGui.QMessageBox.critical(self, u'警告', u'SMTP端口不能为空！')
            return
         
        if self.userEdit.text()=='':
            QtGui.QMessageBox.critical(self, u'警告', u'SMTP用户名不能为空！')
            return
        
        if self.pwdEdit.text()=='':
            QtGui.QMessageBox.critical(self, u'警告', u'用户密码不能为空！')
            return
        
        if self.postfixEdit.text()=='':
            QtGui.QMessageBox.critical(self, u'警告', u'邮箱后缀不能为空！')
            return
        
        cfg = ConfigParser.ConfigParser()
        cfg.read('setting.ini')
        
        cfg.set('setting', 'host', str(self.hostEdit.text()).strip())
        cfg.set('setting', 'port', str(self.portEdit.text()).strip())
        cfg.set('setting', 'user', str(self.userEdit.text()).strip())
        cfg.set('setting', 'pass', str(self.pwdEdit.text()).strip())
        cfg.set('setting', 'postfix', str(self.postfixEdit.text()).strip())
        cfg.write(open('setting.ini', 'w'))
        
        self.close()
       

# 选项窗口类
class OptionsWindow(QtGui.QDialog):
    def __init__(self, parent=None):
        QtGui.QDialog.__init__(self, parent)
        self.setWindowTitle(u'其它参数设置')
        self.setWindowIcon(QtGui.QIcon('icons/email.ico'))
        self.layout()
        self.loadSetting()
 
    # 设置窗口布局
    def layout(self):
        threadnum = QtGui.QLabel(u'线程数：')
        sendinterval = QtGui.QLabel(u'发送间隔(秒)：')

        self.threadnumEdit = QtGui.QLineEdit()
        self.sendintervalEdit = QtGui.QLineEdit()
        
        save = QtGui.QPushButton(u'保存')
        cancel = QtGui.QPushButton(u'取消')
        
        grid = QtGui.QGridLayout()
        grid.setSpacing(10)
  
        grid.addWidget(threadnum, 1, 0)
        grid.addWidget(self.threadnumEdit, 1, 1, 1, 3)
        
        grid.addWidget(sendinterval, 2, 0)
        grid.addWidget(self.sendintervalEdit, 2, 1, 1, 3)
        
        grid.addWidget(save, 3, 1)
        grid.addWidget(cancel, 3, 2)
        
        self.connect(cancel, QtCore.SIGNAL('clicked()'), QtCore.SLOT('close()'))
        self.connect(save, QtCore.SIGNAL('clicked()'), self.saveSetting)
        
        self.setLayout(grid)
        self.setFixedSize(300, 180)
        self.center()
       
    # 设置窗口居中
    def center(self):
        screen = QtGui.QDesktopWidget().screenGeometry()
        size = self.geometry()
        self.move((screen.width()-size.width())/2, (screen.height()-size.height())/2)
    
    # 加载设置
    def loadSetting(self):
        cfg = ConfigParser.ConfigParser()
        cfg.read('setting.ini')
        
        try:
            threadnum = cfg.get('setting', 'threadnum')
        except Exception, e:
            threadnum = '2'
        
        try:
            sendinterval = cfg.get('setting', 'sendinterval')
        except Exception, e:
            sendinterval = '2'

        self.threadnumEdit.setText(threadnum)
        self.sendintervalEdit.setText(sendinterval)

    # 保存设置
    def saveSetting(self):
        cfg = ConfigParser.ConfigParser()
        cfg.read('setting.ini')
        
        cfg.set('setting', 'threadnum', str(self.threadnumEdit.text()).strip() or '2')
        cfg.set('setting', 'sendinterval', str(self.sendintervalEdit.text()).strip() or '2')
        cfg.write(open('setting.ini', 'w'))
        
        self.close()

# 转unicode函数
def to_unicode(obj, encoding='gbk'):
    """Convert obj to unicode
    """
    if isinstance(obj, basestring):
        if not isinstance(obj, unicode):
            obj = obj.decode(encoding, 'ignore')
    return obj


# 发送邮件函数，支持邮件列表
def send_mail(to_list, sub, content, charset, sender, mail_host, mail_port, mail_user, mail_pass, mail_postfix):
    """send email
    
    to_list - receiver list
    sub - mail subject
    content - mail content
    """
    me = sender + "<" + mail_user + "@" + mail_postfix + ">"
    msg = MIMEText(content, 'html', charset)     
    
    msg['Subject'] = sub
    msg['From'] = me
    msg['To'] = ";".join(to_list)
    
    try:
        s = smtplib.SMTP()
        s.connect(mail_host, mail_port)
        s.login(mail_user, mail_pass)
        s.sendmail(me, to_list, msg.as_string())
        s.close()
        return True
    except Exception, e:
        return str(e)
    

if __name__ == '__main__':
    app = QtGui.QApplication(sys.argv)
    main = MainWindow()
    main.show()
    sys.exit(app.exec_())
    
    